package org.mx.i8n;

import org.mx.StringUtils;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.support.AbstractResourceBasedMessageSource;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.lang.Nullable;

import java.text.MessageFormat;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 描述： 面向DS特有的多语言数据表配置方式的消息源定义类
 *
 * @author john peng Date time 2019/8/14 5:47 PM
 */
public class ReloadableDsDbResourceBundleMessageSource extends AbstractResourceBasedMessageSource
		implements ResourceLoaderAware {
	private final ConcurrentMap<Locale, ReloadableDsDbResourceBundleMessageSource.PropertiesHolder> cachedProperties = new ConcurrentHashMap<>();
	private boolean concurrentRefresh = true;
	// private ResourceLoader resourceLoader = new DefaultResourceLoader();
	private String tableName;
	private String tablePrimary;
	// key: lang, value: lang field name
	private Map<String, String> langFields;
	private JdbcTemplate jdbcTemplate;

	/**
	 * 设置表名
	 * 
	 * @param tableName 表面
	 */
	public void setTableName(String tableName) {
		this.tableName = tableName;
	}

	/**
	 * 设置表主键
	 * 
	 * @param tablePrimary 表关键字
	 */
	public void setTablePrimary(String tablePrimary) {
		this.tablePrimary = tablePrimary;
	}

	/**
	 * 设置语言字段集合
	 * 
	 * @param langFields 语言字段集合
	 */
	public void setLangFields(Map<String, String> langFields) {
		this.langFields = langFields;
	}

	/**
	 * 设置JDBC模版对象
	 * 
	 * @param jdbcTemplate JdbcTemplate
	 */
	public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
		this.jdbcTemplate = jdbcTemplate;
	}

	/**
	 * 设置是否并发刷新
	 * 
	 * @param concurrentRefresh 是否并发刷新
	 */
	public void setConcurrentRefresh(boolean concurrentRefresh) {
		this.concurrentRefresh = concurrentRefresh;
	}

	@Override
	public void setResourceLoader(@Nullable ResourceLoader resourceLoader) {
		// this.resourceLoader = (resourceLoader != null ? resourceLoader : new
		// DefaultResourceLoader());
		// do nothing
	}

	@Override
	protected String resolveCodeWithoutArguments(String code, Locale locale) {
		ReloadableDsDbResourceBundleMessageSource.PropertiesHolder propHolder = getProperties(locale);
		return propHolder.getProperty(code);
	}

	@Override
	@Nullable
	protected MessageFormat resolveCode(String code, Locale locale) {
		ReloadableDsDbResourceBundleMessageSource.PropertiesHolder propHolder = getProperties(locale);
		return propHolder.getMessageFormat(code, locale);
	}

	/**
	 * 获取Properties
	 * 
	 * @param locale 本地化对象
	 * @return PropertiesHolder
	 */
	protected ReloadableDsDbResourceBundleMessageSource.PropertiesHolder getProperties(Locale locale) {
		ReloadableDsDbResourceBundleMessageSource.PropertiesHolder propHolder = this.cachedProperties.get(locale);
		long originalTimestamp = -2;

		if (propHolder != null) {
			originalTimestamp = propHolder.getRefreshTimestamp();
			if (originalTimestamp > 0 && originalTimestamp >= System.currentTimeMillis() - getCacheMillis()) {
				// Up to date
				return propHolder;
			}
		} else {
			propHolder = new ReloadableDsDbResourceBundleMessageSource.PropertiesHolder();
			ReloadableDsDbResourceBundleMessageSource.PropertiesHolder existingHolder = this.cachedProperties
					.putIfAbsent(locale, propHolder);
			if (existingHolder != null) {
				propHolder = existingHolder;
			}
		}

		// At this point, we need to refresh...
		if (this.concurrentRefresh && propHolder.getRefreshTimestamp() >= 0) {
			// A populated but stale holder -> could keep using it.
			if (!propHolder.refreshLock.tryLock()) {
				// Getting refreshed by another thread already ->
				// let's return the existing properties for the time being.
				return propHolder;
			}
		} else {
			propHolder.refreshLock.lock();
		}
		try {
			ReloadableDsDbResourceBundleMessageSource.PropertiesHolder existingHolder = this.cachedProperties
					.get(locale);
			originalTimestamp = existingHolder.getRefreshTimestamp();
			if (originalTimestamp > 0 && originalTimestamp >= System.currentTimeMillis() - getCacheMillis()) {
				return existingHolder;
			}
			if (logger.isDebugEnabled()) {
				logger.debug(String.format(
						"Refresh message resource[%s] from database, last refresh: %d, current: %d, timeout: %d.",
						locale.getLanguage(), originalTimestamp, System.currentTimeMillis(), getCacheMillis()));
			}
			return refreshProperties(locale, propHolder);
		} finally {
			propHolder.refreshLock.unlock();
		}
	}

	private String getLangField(Locale locale) {
		String name1 = locale.getLanguage();
		String name2 = String.format("%s_%s", locale.getLanguage(), locale.getCountry());
		if (langFields.containsKey(name1)) {
			return langFields.get(name1);
		} else {
			return langFields.getOrDefault(name2, null);
		}
	}

	private Properties loadPropertiesFromDsDb(Locale locale) {
		Properties properties = new Properties();
		String fieldName = getLangField(locale);
		if (StringUtils.isBlank(fieldName)) {
			if (logger.isWarnEnabled()) {
				logger.warn(String.format("The field not existed for language[%s].", locale.getLanguage()));
			}
			return properties;
		}
		String sql = String.format("SELECT %s, %s FROM %s", tablePrimary, fieldName, tableName);
		String[] primaryFields = StringUtils.split(tablePrimary, true, true);
		jdbcTemplate.query(sql, rs -> {
			StringBuffer sb = new StringBuffer();
			for (String field : primaryFields) {
				String fieldValue = rs.getString(field);
				sb.append(fieldValue);
				sb.append(".");
			}
			String message = rs.getString(fieldName);
			String key = sb.toString();
			if (key.endsWith(".")) {
				key = key.substring(0, key.length() - 1);
			}
			properties.put(key, message == null ? "" : message);
		});
		return properties;
	}

	/**
	 * 刷新properties资源
	 * 
	 * @param locale     本地化对象
	 * @param propHolder PropertiesHolder对象
	 * @return PropertiesHolder对象
	 */
	protected ReloadableDsDbResourceBundleMessageSource.PropertiesHolder refreshProperties(Locale locale,
			@Nullable ReloadableDsDbResourceBundleMessageSource.PropertiesHolder propHolder) {
		long refreshTimestamp = System.currentTimeMillis();
		try {
			Properties props = loadPropertiesFromDsDb(locale);
			propHolder = new ReloadableDsDbResourceBundleMessageSource.PropertiesHolder(props, refreshTimestamp);
		} catch (Exception ex) {
			if (logger.isWarnEnabled()) {
				logger.warn(String.format("Could not load message source from dsdb with language[%s].",
						locale.getDisplayName()), ex);
			}
			propHolder = new ReloadableDsDbResourceBundleMessageSource.PropertiesHolder();
		}
		propHolder.setRefreshTimestamp(refreshTimestamp);
		this.cachedProperties.put(locale, propHolder);
		return propHolder;
	}

	/**
	 * 清除缓冲内容
	 */
	public void clearCache() {
		logger.debug("Clearing entire resource bundle cache");
		this.cachedProperties.clear();
	}

	/**
	 * 清除缓冲资源
	 */
	public void clearCacheIncludingAncestors() {
		clearCache();
		if (getParentMessageSource() instanceof ReloadableResourceBundleMessageSource) {
			((ReloadableResourceBundleMessageSource) getParentMessageSource()).clearCacheIncludingAncestors();
		}
	}

	@Override
	public String toString() {
		return getClass().getName() + ": basenames=" + getBasenameSet();
	}

	/**
	 * PropertiesHolder类
	 * 
	 * @author john.peng
	 *
	 */
	protected class PropertiesHolder {

		@Nullable
		private final Properties properties;

		private final long fileTimestamp;
		private final ReentrantLock refreshLock = new ReentrantLock();
		private final ConcurrentMap<String, Map<Locale, MessageFormat>> cachedMessageFormats = new ConcurrentHashMap<>();
		private volatile long refreshTimestamp = -2;

		/**
		 * 默认的构造函数
		 */
		public PropertiesHolder() {
			this.properties = null;
			this.fileTimestamp = -1;
		}

		/**
		 * 构造函数
		 * 
		 * @param properties    Properties对象
		 * @param fileTimestamp 文件时间戳
		 */
		public PropertiesHolder(Properties properties, long fileTimestamp) {
			this.properties = properties;
			this.fileTimestamp = fileTimestamp;
		}

		/**
		 * 获取Properties对象
		 * 
		 * @return Properties对象
		 */
		@Nullable
		public Properties getProperties() {
			return this.properties;
		}

		/**
		 * 获取文件时间戳
		 * 
		 * @return 时间戳
		 */
		public long getFileTimestamp() {
			return this.fileTimestamp;
		}

		/**
		 * 获取刷新时间戳
		 * 
		 * @return 时间戳
		 */
		public long getRefreshTimestamp() {
			return this.refreshTimestamp;
		}

		/**
		 * 设置刷新时间戳
		 * 
		 * @param refreshTimestamp 时间戳
		 */
		public void setRefreshTimestamp(long refreshTimestamp) {
			this.refreshTimestamp = refreshTimestamp;
		}

		/**
		 * 获取属性
		 * 
		 * @param code 代码
		 * @return 属性值
		 */
		@Nullable
		public String getProperty(String code) {
			if (this.properties == null) {
				return null;
			}
			return this.properties.getProperty(code);
		}

		/**
		 * 获取消息格式
		 * 
		 * @param code   代码
		 * @param locale 本地化对象
		 * @return 消息格式
		 */
		@Nullable
		public MessageFormat getMessageFormat(String code, Locale locale) {
			if (this.properties == null) {
				return null;
			}
			Map<Locale, MessageFormat> localeMap = this.cachedMessageFormats.get(code);
			if (localeMap != null) {
				MessageFormat result = localeMap.get(locale);
				if (result != null) {
					return result;
				}
			}
			String msg = this.properties.getProperty(code);
			if (msg != null) {
				if (localeMap == null) {
					localeMap = new ConcurrentHashMap<>();
					Map<Locale, MessageFormat> existing = this.cachedMessageFormats.putIfAbsent(code, localeMap);
					if (existing != null) {
						localeMap = existing;
					}
				}
				MessageFormat result = createMessageFormat(msg, locale);
				localeMap.put(locale, result);
				return result;
			}
			return null;
		}
	}
}
